<?php

namespace Tcnet\Pay\Gateways\Ttpay;

use Exception;
use Tcnet\Pay\Events;
use Tcnet\Pay\Exceptions\BusinessException;
use Tcnet\Pay\Exceptions\GatewayException;
use Tcnet\Pay\Exceptions\InvalidArgumentException;
use Tcnet\Pay\Exceptions\InvalidConfigException;
use Tcnet\Pay\Exceptions\InvalidSignException;
use Tcnet\Pay\Gateways\Ttpay;
use Tcnet\Pay\Gateways\Wechat;
use Tcnet\Pay\Log;
use Yansongda\Supports\Arr;
use Yansongda\Supports\Collection;
use Yansongda\Supports\Config;
use Yansongda\Supports\Str;
use Yansongda\Supports\Traits\HasHttpRequest;

/**
 * @author tc-net <tc_net@163.com>
 *
 * @property string app_id alipay app_id
 * @property string salt
 * @property array http http options
 * @property string mode current mode
 * @property array log log options
 * @property string pid ali pid
 */
class Support
{
    use HasHttpRequest;

    /**
     * Ttpay gateway.
     *
     * @var string
     */
    protected $baseUri;

    /**
     * Config.
     *
     * @var Config
     */
    protected $config;

    /**
     * Instance.
     *
     * @var Support
     */
    private static $instance;

    /**
     * Bootstrap.
     *
     * @author tc-net <tc_net@163.com>
     */
    private function __construct(Config $config)
    {
        $this->baseUri = Ttpay::URL[$config->get('mode', Ttpay::MODE_NORMAL)];
        $this->config = $config;

        $this->setHttpOptions();
    }

    /**
     * __get.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param $key
     *
     * @return mixed|Config|null
     */
    public function __get($key)
    {
        return $this->getConfig($key);
    }

    /**
     * create.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @return Support
     */
    public static function create(Config $config)
    {
        if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
            self::$instance = new self($config);
        }

        return self::$instance;
    }

    /**
     * getInstance.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @throws InvalidArgumentException
     *
     * @return Support
     */
    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            throw new InvalidArgumentException('You Should [Create] First Before Using');
        }

        return self::$instance;
    }

    /**
     * clear.
     *
     * @author tc-net <tc_net@163.com>
     */
    public function clear()
    {
        self::$instance = null;
    }

    /**
     * Get Ttpay API result.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @throws GatewayException
     * @throws InvalidConfigException
     * @throws InvalidSignException
     */
    public static function requestApi($endpoint,array $data, bool $response = false): Collection
    {
        Events::dispatch(new Events\ApiRequesting('Ttpay', '', self::$instance->getBaseUri(), $data));
        $options = [
            'headers' => [
                'Content-Type' => 'application/json',
            ],
        ];
        $data = array_filter($data, function ($value) {
            return ('' == $value || is_null($value)) ? false : true;
        });
        $result = self::$instance->post($endpoint, json_encode($data,JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE),$options);

        Events::dispatch(new Events\ApiRequested('Ttpay', '', self::$instance->getBaseUri().$endpoint, $result));
        if($endpoint=="query_order"){
            return self::processingApiResultInfo($data, $result, $response);
        }else{
            return self::processingApiResult($data, $result, $response);
        }
    }

    /**
     * Filter payload.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param array        $payload
     * @param array|string $params
     * @param bool         $preserve_notify_url
     *
     * @throws InvalidArgumentException
     */
    public static function filterPayload($payload, $params, $preserve_notify_url = false): array
    {
        $payload = array_merge(
            $payload,
            is_array($params) ? $params : ['out_order_no' => $params]
        );
        $payload = array_filter($payload, function ($value) {
            return ('' == $value || is_null($value)) ? false : true;
        });
//        $payload['appid'] = self::$instance->getConfig($type, '');
//
//        if (Wechat::MODE_SERVICE === self::$instance->getConfig('mode', Wechat::MODE_NORMAL)) {
//            $payload['sub_appid'] = self::$instance->getConfig('sub_'.$type, '');
//        }
//
//        unset($payload['trade_type'], $payload['type']);
//        if (!$preserve_notify_url) {
//            unset($payload['notify_url']);
//        }

        $payload['sign'] = self::generateSign($payload);

        return $payload;
    }

    private function isArrMap($map) {
        foreach($map as $k =>$v) {
            if (is_string($k)){
                return true;
            }
        }

        return false;
    }

    private function arrayToStr($map) {
        $isMap = self::isArrMap($map);

        $result = "";
        if ($isMap){
            $result = "map[";
        }

        $keyArr = array_keys($map);
        if ($isMap) {
            sort($keyArr);
        }

        $paramsArr = array();
        foreach($keyArr as  $k) {
            $v = $map[$k];
            if ($isMap) {
                if (is_array($v)) {
                    $paramsArr[] = sprintf("%s:%s", $k, arrayToStr($v));
                } else  {
                    $paramsArr[] = sprintf("%s:%s", $k, trim(strval($v)));
                }
            } else {
                if (is_array($v)) {
                    $paramsArr[] = arrayToStr($v);
                } else  {
                    $paramsArr[] = trim(strval($v));
                }
            }
        }

        $result = sprintf("%s%s", $result, join(" ", $paramsArr));
        $result = sprintf("[%s]", $result);

        return $result;
    }

    /**
     * 回调通知
     * @param array $content
     * @return string
     */
    public static function generateNotifySign(array $content){
        $rList = array();
        $token = self::$instance->token;
        array_push($rList, $token);
        foreach($content as $k =>$v) {
            if ( $k == "type" || $k=='msg_signature')
                continue;
            $value = trim(strval($v));
            if ($value == "" || $value == "null")
                continue;
            array_push($rList, $value);
        }
        sort($rList,2);
        return sha1(implode($rList));
    }
    /**
     * Generate sign.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @throws InvalidConfigException
     */
    public static function generateSign(array $params): string
    {
        $salt = self::$instance->salt;
        $rList = [];
        foreach($params as $k =>$v) {
            if ($k == "other_settle_params" || $k == "app_id" || $k == "sign" || $k == "thirdparty_id")
                continue;

            $value = trim(strval($v));
            if (is_array($v)) {
                $value = self::arrayToStr($v);
            }

            $len = strlen($value);
            if ($len > 1 && substr($value, 0,1)=="\"" && substr($value,$len, $len-1)=="\"")
                $value = substr($value,1, $len-1);
            $value = trim($value);
            if ($value == "" || $value == "null")
                continue;
            $rList[] = $value;
        }
        $rList[] = $salt;
        sort($rList, SORT_STRING);
        $sign = md5(implode('&', $rList));

        return $sign;
    }

    /**
     * Verify sign.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param bool        $sync
     * @param string|null $sign
     *
     * @throws InvalidConfigException
     */
    public static function verifySign(array $data, $sync = false, $sign = null): bool
    {
        $publicKey = self::$instance->ali_public_key;

        if (is_null($publicKey)) {
            throw new InvalidConfigException('Missing Ttpay Config -- [ali_public_key]');
        }

        if (Str::endsWith($publicKey, '.crt')) {
            $publicKey = file_get_contents($publicKey);
        } elseif (Str::endsWith($publicKey, '.pem')) {
            $publicKey = openssl_pkey_get_public(
                Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey
            );
        } else {
            $publicKey = "-----BEGIN PUBLIC KEY-----\n".
                wordwrap($publicKey, 64, "\n", true).
                "\n-----END PUBLIC KEY-----";
        }

        $sign = $sign ?? $data['sign'];

        $toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true);

        $isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);

        if (is_resource($publicKey)) {
            openssl_free_key($publicKey);
        }

        return $isVerify;
    }

    /**
     * Get signContent that is to be signed.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param bool $verify
     */
    public static function getSignContent(array $data, $verify = false): string
    {
        ksort($data);

        $stringToBeSigned = '';
        foreach ($data as $k => $v) {
            if ($verify && 'sign' != $k && 'sign_type' != $k) {
                $stringToBeSigned .= $k.'='.$v.'&';
            }
            if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) {
                $stringToBeSigned .= $k.'='.$v.'&';
            }
        }

        Log::debug('Ttpay Generate Sign Content Before Trim', [$data, $stringToBeSigned]);

        return trim($stringToBeSigned, '&');
    }

    /**
     * Convert encoding.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param string|array $data
     * @param string       $to
     * @param string       $from
     */
    public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array
    {
        return Arr::encoding((array) $data, $to, $from);
    }

    /**
     * Get service config.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param string|null $key
     * @param mixed|null  $default
     *
     * @return mixed|null
     */
    public function getConfig($key = null, $default = null)
    {
        if (is_null($key)) {
            return $this->config->all();
        }

        if ($this->config->has($key)) {
            return $this->config[$key];
        }

        return $default;
    }

    /**
     * Get Base Uri.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @return string
     */
    public function getBaseUri()
    {
        return $this->baseUri;
    }
    /**
     * processingApiResult.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param $data
     * @param $result
     *
     * @throws GatewayException
     * @throws InvalidConfigException
     * @throws InvalidSignException
     */
    protected static function processingApiResult($data, $result, $response = false): Collection
    {
        if ($response) {
            return new Collection($result);
        }
        if (!isset($result['err_no']) || 0 != $result['err_no']) {
            throw new GatewayException('Get Ttpay API Error:'.($result['err_tips'] ?? $result['err_tips'] ?? ''), $result);
        }
        return new Collection($result['data']);
    }
    /**
     * processingApiResultInfo.
     *
     * @author tc-net <tc_net@163.com>
     *
     * @param $data
     * @param $result
     *
     * @throws GatewayException
     * @throws InvalidConfigException
     * @throws InvalidSignException
     */
    protected static function processingApiResultInfo($data, $result, $response = false): Collection
    {
        if ($response) {
            return new Collection($result);
        }
        if (!isset($result['err_no']) || 0 != $result['err_no']) {
            throw new GatewayException('Get Ttpay API Error:'.($result['err_tips'] ?? $result['err_tips'] ?? ''), $result);
        }
        $data = $result['payment_info'];
        $data['out_order_no'] = $result['out_order_no'];
        $data['order_id'] = $result['order_id'];
        return new Collection($data);
    }

    /**
     * Set Http options.
     *
     * @author tc-net <tc_net@163.com>
     */
    protected function setHttpOptions(): self
    {
        if ($this->config->has('http') && is_array($this->config->get('http'))) {
            $this->config->forget('http.base_uri');
            $this->httpOptions = $this->config->get('http');
        }

        return $this;
    }

    /**
     * 0x转高精度数字.
     *
     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
     *
     * @param $hex
     *
     * @return int|string
     */
    private static function bchexdec($hex)
    {
        $dec = 0;
        $len = strlen($hex);
        for ($i = 1; $i <= $len; ++$i) {
            if (ctype_xdigit($hex[$i - 1])) {
                $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
            }
        }

        return str_replace('.00', '', $dec);
    }
}
